/****************************************************************
* @file   : eth_func.cpp
* @author : NingJian (freegodly@gmail.com)
* @date   : 05/26/21 09:37:01
* @brief   : 
****************************************************************/



#include "eth_func.hpp"
#include <cassert>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <memory>
#include <mutex>
#include <stdlib.h>
#include <byteswap.h>
#include <iostream>
#include <chrono>
#include <future>
#include <vector>
#include <thread>

namespace eth { 
namespace mcore {


std::mutex shared_eth_context_mutex;
std::shared_ptr<eth_context> shared_eth_context;

static const int epoch_length = ETHASH_EPOCH_LENGTH;
static const int light_cache_item_size = ETHASH_LIGHT_CACHE_ITEM_SIZE;
static const int full_dataset_item_size = ETHASH_FULL_DATASET_ITEM_SIZE;
static const int num_dataset_accesses = ETHASH_NUM_DATASET_ACCESSES;
static const int light_cache_init_size = 1 << 24;
static const int light_cache_growth = 1 << 17;
static const int light_cache_rounds = 3;
static const int full_dataset_init_size = 1 << 30;
static const int full_dataset_growth = 1 << 23;
static const int full_dataset_item_parents = 256;

const static size_t l1_cache_size = 16 * 1024;
const static size_t l1_cache_num_items = l1_cache_size / sizeof(uint32_t);


/** FNV 32-bit prime. */
static const uint32_t fnv_prime = 0x01000193;

#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
#define to_le64(X) __builtin_bswap64(X)
#define to_le32(X) __builtin_bswap32(X)
#else
#define to_le64(X) X
#define to_le32(X) X
#endif


static inline uint64_t rol(uint64_t x, unsigned s)
{
    return (x << s) | (x >> (64 - s));
}

static const uint64_t round_constants[24] = { 
    0x0000000000000001, 0x0000000000008082, 0x800000000000808a, 0x8000000080008000,
    0x000000000000808b, 0x0000000080000001, 0x8000000080008081, 0x8000000000008009,
    0x000000000000008a, 0x0000000000000088, 0x0000000080008009, 0x000000008000000a,
    0x000000008000808b, 0x800000000000008b, 0x8000000000008089, 0x8000000000008003,
    0x8000000000008002, 0x8000000000000080, 0x000000000000800a, 0x800000008000000a,
    0x8000000080008081, 0x8000000000008080, 0x0000000080000001, 0x8000000080008008};

hash256 hash256_from_bytes(const uint8_t bytes[32]) 
{
    hash256 h;
    std::memcpy(&h, bytes, sizeof(h));
    return h;
}

int fromHex(char _i)
{
    if (_i >= '0' && _i <= '9')
        return _i - '0';
    if (_i >= 'a' && _i <= 'f')
        return _i - 'a' + 10;
    if (_i >= 'A' && _i <= 'F')
        return _i - 'A' + 10;
    else
        return -1;
}

char toHex(int _i)
{
    if (_i >= 0 && _i <= 9)
        return _i + '0';
    if (_i >= 10 && _i <= 15)
        return _i + 'A' - 10;
    else
        return '#';
}

hash256 hash256_from_string(const std::string& str) 
{
    hash256 h256;

    int s = (str[0] == '0' && str[1] == 'x') ? 2 : 0;

    for (int i = s; i < str.size(); i += 2)
    {
        int h = fromHex(str[i]);
        int l = fromHex(str[i + 1]);
        if (h != -1 && l != -1)
            h256.bytes[i/2-s/2] = h * 16 + l;
        else
            std::runtime_error("hash256_from_string bad input!");
    }
    return h256;
}

std::string hash256_to_string(const hash256& h256) 
{

    std::string h_str="0x";

    for (unsigned i = 0; i < 32; ++i)
    {
       h_str.push_back(toHex(h256.bytes[i]/16));
       h_str.push_back(toHex(h256.bytes[i]%16));
    }
    return std::move(h_str);
}

std::string hash512_to_string(const hash512& h256){
    
    std::string h_str="0x";

    for (unsigned i = 0; i < 64; ++i)
    {
       h_str.push_back(toHex(h256.bytes[i]/16));
       h_str.push_back(toHex(h256.bytes[i]%16));
    }
    return std::move(h_str);
}

std::string hash1024_to_string(const hash1024& h256)
{
    
    std::string h_str="0x";

    for (unsigned i = 0; i < 128; ++i)
    {
       h_str.push_back(toHex(h256.bytes[i]/16));
       h_str.push_back(toHex(h256.bytes[i]%16));
    }
    return std::move(h_str);
}

std::string hash2048_to_string(const hash2048& h256)
{
    
    std::string h_str="0x";

    for (unsigned i = 0; i < 256; ++i)
    {
       h_str.push_back(toHex(h256.bytes[i]/16));
       h_str.push_back(toHex(h256.bytes[i]%16));
    }
    return std::move(h_str);
}

int find_epoch_number(const hash256& seed) 
{
    static const int num_tries = 30000;  // Divisible by 16.

    // Thread-local cache of the last search.
    static thread_local int cached_epoch_number = 0;
    static thread_local hash256 cached_seed = {};

    // Load from memory once (memory will be clobbered by keccak256()).
    const uint32_t seed_part = seed.word32s[0];
    const int e = cached_epoch_number;
    hash256 s = cached_seed;

    if (s.word32s[0] == seed_part)
        return e;

    // Try the next seed, will match for sequential epoch access.
    s = keccak256(s);
    if (s.word32s[0] == seed_part)
    {
        cached_seed = s;
        cached_epoch_number = e + 1;
        return e + 1;
    }

    // Search for matching seed starting from epoch 0.
    s = {};
    for (int i = 0; i < num_tries; ++i)
    {
        if (s.word32s[0] == seed_part)
        {
            cached_seed = s;
            cached_epoch_number = i;
            return i;
        }

        s = keccak256(s);
    }

    return -1;
}


hash256 keccak256(const hash256& input) 
{
    hash256 hash;
    keccak(hash.word64s, 256, input.bytes, 32);
    return hash;
}


uint64_t load_le(const uint8_t* data)
{
    uint64_t word;
    memcpy(&word, data, sizeof(word));
    return to_le64(word);
}

void keccakf1600(uint64_t state[25])
{
    uint64_t Aba, Abe, Abi, Abo, Abu;
    uint64_t Aga, Age, Agi, Ago, Agu;
    uint64_t Aka, Ake, Aki, Ako, Aku;
    uint64_t Ama, Ame, Ami, Amo, Amu;
    uint64_t Asa, Ase, Asi, Aso, Asu;

    uint64_t Eba, Ebe, Ebi, Ebo, Ebu;
    uint64_t Ega, Ege, Egi, Ego, Egu;
    uint64_t Eka, Eke, Eki, Eko, Eku;
    uint64_t Ema, Eme, Emi, Emo, Emu;
    uint64_t Esa, Ese, Esi, Eso, Esu;

    uint64_t Ba, Be, Bi, Bo, Bu;

    uint64_t Da, De, Di, Do, Du;

    Aba = state[0];
    Abe = state[1];
    Abi = state[2];
    Abo = state[3];
    Abu = state[4];
    Aga = state[5];
    Age = state[6];
    Agi = state[7];
    Ago = state[8];
    Agu = state[9];
    Aka = state[10];
    Ake = state[11];
    Aki = state[12];
    Ako = state[13];
    Aku = state[14];
    Ama = state[15];
    Ame = state[16];
    Ami = state[17];
    Amo = state[18];
    Amu = state[19];
    Asa = state[20];
    Ase = state[21];
    Asi = state[22];
    Aso = state[23];
    Asu = state[24];

    for (size_t n = 0; n < 24; n += 2)
    {
        // Round (n + 0): Axx -> Exx

        Ba = Aba ^ Aga ^ Aka ^ Ama ^ Asa;
        Be = Abe ^ Age ^ Ake ^ Ame ^ Ase;
        Bi = Abi ^ Agi ^ Aki ^ Ami ^ Asi;
        Bo = Abo ^ Ago ^ Ako ^ Amo ^ Aso;
        Bu = Abu ^ Agu ^ Aku ^ Amu ^ Asu;

        Da = Bu ^ rol(Be, 1);
        De = Ba ^ rol(Bi, 1);
        Di = Be ^ rol(Bo, 1);
        Do = Bi ^ rol(Bu, 1);
        Du = Bo ^ rol(Ba, 1);

        Ba = Aba ^ Da;
        Be = rol(Age ^ De, 44);
        Bi = rol(Aki ^ Di, 43);
        Bo = rol(Amo ^ Do, 21);
        Bu = rol(Asu ^ Du, 14);
        Eba = Ba ^ (~Be & Bi) ^ round_constants[n];
        Ebe = Be ^ (~Bi & Bo);
        Ebi = Bi ^ (~Bo & Bu);
        Ebo = Bo ^ (~Bu & Ba);
        Ebu = Bu ^ (~Ba & Be);

        Ba = rol(Abo ^ Do, 28);
        Be = rol(Agu ^ Du, 20);
        Bi = rol(Aka ^ Da, 3);
        Bo = rol(Ame ^ De, 45);
        Bu = rol(Asi ^ Di, 61);
        Ega = Ba ^ (~Be & Bi);
        Ege = Be ^ (~Bi & Bo);
        Egi = Bi ^ (~Bo & Bu);
        Ego = Bo ^ (~Bu & Ba);
        Egu = Bu ^ (~Ba & Be);

        Ba = rol(Abe ^ De, 1);
        Be = rol(Agi ^ Di, 6);
        Bi = rol(Ako ^ Do, 25);
        Bo = rol(Amu ^ Du, 8);
        Bu = rol(Asa ^ Da, 18);
        Eka = Ba ^ (~Be & Bi);
        Eke = Be ^ (~Bi & Bo);
        Eki = Bi ^ (~Bo & Bu);
        Eko = Bo ^ (~Bu & Ba);
        Eku = Bu ^ (~Ba & Be);

        Ba = rol(Abu ^ Du, 27);
        Be = rol(Aga ^ Da, 36);
        Bi = rol(Ake ^ De, 10);
        Bo = rol(Ami ^ Di, 15);
        Bu = rol(Aso ^ Do, 56);
        Ema = Ba ^ (~Be & Bi);
        Eme = Be ^ (~Bi & Bo);
        Emi = Bi ^ (~Bo & Bu);
        Emo = Bo ^ (~Bu & Ba);
        Emu = Bu ^ (~Ba & Be);

        Ba = rol(Abi ^ Di, 62);
        Be = rol(Ago ^ Do, 55);
        Bi = rol(Aku ^ Du, 39);
        Bo = rol(Ama ^ Da, 41);
        Bu = rol(Ase ^ De, 2);
        Esa = Ba ^ (~Be & Bi);
        Ese = Be ^ (~Bi & Bo);
        Esi = Bi ^ (~Bo & Bu);
        Eso = Bo ^ (~Bu & Ba);
        Esu = Bu ^ (~Ba & Be);


        // Round (n + 1): Exx -> Axx

        Ba = Eba ^ Ega ^ Eka ^ Ema ^ Esa;
        Be = Ebe ^ Ege ^ Eke ^ Eme ^ Ese;
        Bi = Ebi ^ Egi ^ Eki ^ Emi ^ Esi;
        Bo = Ebo ^ Ego ^ Eko ^ Emo ^ Eso;
        Bu = Ebu ^ Egu ^ Eku ^ Emu ^ Esu;

        Da = Bu ^ rol(Be, 1);
        De = Ba ^ rol(Bi, 1);
        Di = Be ^ rol(Bo, 1);
        Do = Bi ^ rol(Bu, 1);
        Du = Bo ^ rol(Ba, 1);

        Ba = Eba ^ Da;
        Be = rol(Ege ^ De, 44);
        Bi = rol(Eki ^ Di, 43);
        Bo = rol(Emo ^ Do, 21);
        Bu = rol(Esu ^ Du, 14);
        Aba = Ba ^ (~Be & Bi) ^ round_constants[n + 1];
        Abe = Be ^ (~Bi & Bo);
        Abi = Bi ^ (~Bo & Bu);
        Abo = Bo ^ (~Bu & Ba);
        Abu = Bu ^ (~Ba & Be);

        Ba = rol(Ebo ^ Do, 28);
        Be = rol(Egu ^ Du, 20);
        Bi = rol(Eka ^ Da, 3);
        Bo = rol(Eme ^ De, 45);
        Bu = rol(Esi ^ Di, 61);
        Aga = Ba ^ (~Be & Bi);
        Age = Be ^ (~Bi & Bo);
        Agi = Bi ^ (~Bo & Bu);
        Ago = Bo ^ (~Bu & Ba);
        Agu = Bu ^ (~Ba & Be);

        Ba = rol(Ebe ^ De, 1);
        Be = rol(Egi ^ Di, 6);
        Bi = rol(Eko ^ Do, 25);
        Bo = rol(Emu ^ Du, 8);
        Bu = rol(Esa ^ Da, 18);
        Aka = Ba ^ (~Be & Bi);
        Ake = Be ^ (~Bi & Bo);
        Aki = Bi ^ (~Bo & Bu);
        Ako = Bo ^ (~Bu & Ba);
        Aku = Bu ^ (~Ba & Be);

        Ba = rol(Ebu ^ Du, 27);
        Be = rol(Ega ^ Da, 36);
        Bi = rol(Eke ^ De, 10);
        Bo = rol(Emi ^ Di, 15);
        Bu = rol(Eso ^ Do, 56);
        Ama = Ba ^ (~Be & Bi);
        Ame = Be ^ (~Bi & Bo);
        Ami = Bi ^ (~Bo & Bu);
        Amo = Bo ^ (~Bu & Ba);
        Amu = Bu ^ (~Ba & Be);

        Ba = rol(Ebi ^ Di, 62);
        Be = rol(Ego ^ Do, 55);
        Bi = rol(Eku ^ Du, 39);
        Bo = rol(Ema ^ Da, 41);
        Bu = rol(Ese ^ De, 2);
        Asa = Ba ^ (~Be & Bi);
        Ase = Be ^ (~Bi & Bo);
        Asi = Bi ^ (~Bo & Bu);
        Aso = Bo ^ (~Bu & Ba);
        Asu = Bu ^ (~Ba & Be);
    }

    state[0] = Aba;
    state[1] = Abe;
    state[2] = Abi;
    state[3] = Abo;
    state[4] = Abu;
    state[5] = Aga;
    state[6] = Age;
    state[7] = Agi;
    state[8] = Ago;
    state[9] = Agu;
    state[10] = Aka;
    state[11] = Ake;
    state[12] = Aki;
    state[13] = Ako;
    state[14] = Aku;
    state[15] = Ama;
    state[16] = Ame;
    state[17] = Ami;
    state[18] = Amo;
    state[19] = Amu;
    state[20] = Asa;
    state[21] = Ase;
    state[22] = Asi;
    state[23] = Aso;
    state[24] = Asu;
}

void keccak(
    uint64_t* out, size_t bits, const uint8_t* data, size_t size)
{
    static const size_t word_size = sizeof(uint64_t);
    const size_t hash_size = bits / 8;
    const size_t block_size = (1600 - bits * 2) / 8;

    size_t i;
    uint64_t* state_iter;
    uint64_t last_word = 0;
    uint8_t* last_word_iter = (uint8_t*)&last_word;

    uint64_t state[25] = {0};

    while (size >= block_size)
    {
        for (i = 0; i < (block_size / word_size); ++i)
        {
            state[i] ^= load_le(data);
            data += word_size;
        }

        keccakf1600(state);

        size -= block_size;
    }

    state_iter = state;

    while (size >= word_size)
    {
        *state_iter ^= load_le(data);
        ++state_iter;
        data += word_size;
        size -= word_size;
    }

    while (size > 0)
    {
        *last_word_iter = *data;
        ++last_word_iter;
        ++data;
        --size;
    }
    *last_word_iter = 0x01;
    *state_iter ^= to_le64(last_word);

    state[(block_size / word_size) - 1] ^= 0x8000000000000000;

    keccakf1600(state);

    for (i = 0; i < (hash_size / word_size); ++i)
        out[i] = to_le64(state[i]);
}



int find_largest_prime(int upper_bound)
{

   auto is_odd_prime = [](int number) -> int {
        int d;
        for (d = 3; (int64_t)d * (int64_t)d <= (int64_t)number; d += 2)
        {
            if (number % d == 0)
                return 0;
        }
        return 1;
   };

    int n = upper_bound;

    if (n < 2)
        return 0;

    if (n == 2)
        return 2;

    /* If even number, skip it. */
    if (n % 2 == 0)
        --n;

    /* Test descending odd numbers. */
    while (!is_odd_prime(n))
        n -= 2;

    return n;
}


int eth_calculate_light_cache_num_items(int epoch_number)
{
    static const int item_size = sizeof(hash512);
    static const int num_items_init = light_cache_init_size / item_size;
    static const int num_items_growth = light_cache_growth / item_size;
    static_assert(
        light_cache_init_size % item_size == 0, "light_cache_init_size not multiple of item size");
    static_assert(
        light_cache_growth % item_size == 0, "light_cache_growth not multiple of item size");

    int num_items_upper_bound = num_items_init + epoch_number * num_items_growth;
    int num_items = find_largest_prime(num_items_upper_bound);
    return num_items;
}

int eth_calculate_dataset_num_items(int epoch_number)
{
    static const int item_size = sizeof(hash1024);
    static const int num_items_init = full_dataset_init_size / item_size;
    static const int num_items_growth = full_dataset_growth / item_size;
    static_assert(full_dataset_init_size % item_size == 0,
        "full_dataset_init_size not multiple of item size");
    static_assert(
        full_dataset_growth % item_size == 0, "full_dataset_growth not multiple of item size");

    int num_items_upper_bound = num_items_init + epoch_number * num_items_growth;
    int num_items = find_largest_prime(num_items_upper_bound);
    return num_items;
}


hash256 calculate_epoch_seed(int epoch_number){
    hash256 epoch_seed = {};
    for (int i = 0; i < epoch_number; ++i)
        epoch_seed = keccak256(epoch_seed);
    return epoch_seed;
}


hash512 bitwise_xor(const hash512& x, const hash512& y) 
{
    hash512 z;
    for (size_t i = 0; i < sizeof(z) / sizeof(z.word64s[0]); ++i)
        z.word64s[i] = x.word64s[i] ^ y.word64s[i];
    return z;
}

void build_light_cache(hash512 cache[], int num_items, const hash256& seed) 
{
    hash512 item = keccak512(seed.bytes, sizeof(seed));
    cache[0] = item;
    for (int i = 1; i < num_items; ++i)
    {
        item = keccak512(item.bytes, sizeof(item));
        cache[i] = item;
    }

    for (int q = 0; q < light_cache_rounds; ++q)
    {
        for (int i = 0; i < num_items; ++i)
        {
            const uint32_t index_limit = static_cast<uint32_t>(num_items);

            // Fist index: 4 first bytes of the item as little-endian integer.
            const uint32_t t = to_le32(cache[i].word32s[0]);
            const uint32_t v = t % index_limit;

            // Second index.
            const uint32_t w = static_cast<uint32_t>(num_items + (i - 1)) % index_limit;

            const hash512 x = bitwise_xor(cache[v], cache[w]);
            cache[i] = keccak512(x.bytes, sizeof(x));
        }
    }
}


uint32_t fnv1(uint32_t u, uint32_t v)
{
    return (u * fnv_prime) ^ v;
}

hash512 fnv1(const hash512& u, const hash512& v)
{
    hash512 r;
    for (size_t i = 0; i < sizeof(r) / sizeof(r.word32s[0]); ++i)
        r.word32s[i] = fnv1(u.word32s[i], v.word32s[i]);
    return r;
}

/*
多线程初始化dag数据
*/
void  multi_threads_init_full_dataset(eth_context* const context)
{
    hash2048* full_dataset_2048 = reinterpret_cast<hash2048*>((uint32_t*)context->l1_cache);
    const size_t full_dataset_size =static_cast<size_t>(context->full_dataset_num_items) * sizeof(hash1024);

    const size_t total_items = full_dataset_size / sizeof(hash2048);

    size_t num_threads = static_cast<int>(std::thread::hardware_concurrency());
    const uint64_t space_per_thread = total_items / static_cast<uint64_t>(num_threads);

    std::vector<std::thread> mt;
    for(size_t i =0 ; i < num_threads; ++i) {
        std::thread t = std::thread([full_dataset_2048,context](uint32_t start, uint32_t end){
            for (uint32_t j = start; j < end; ++j)
            {
                full_dataset_2048[j] = calculate_dataset_item_2048(*context, j);
            }
        }, i * space_per_thread , i * space_per_thread + space_per_thread  );
        mt.push_back(std::move(t));
    }

    for(size_t i =0 ; i < num_threads; ++i) 
    {
        mt[i].join();
    }

}

eth_context * create_eth_context(int epoch_number,  bool isfull){
    size_t context_alloc_size = sizeof(hash512);
    const size_t light_cache_num_items = eth_calculate_light_cache_num_items(epoch_number);
    const size_t full_dataset_num_items = eth_calculate_dataset_num_items(epoch_number);

    const size_t light_cache_size = static_cast<size_t>(light_cache_num_items) * light_cache_item_size;
    const size_t full_dataset_size =static_cast<size_t>(full_dataset_num_items) * sizeof(hash1024);

    size_t alloc_size = context_alloc_size + light_cache_size;

    if(isfull){
        alloc_size += full_dataset_size;
    }

    char* const alloc_data = static_cast<char*>(std::calloc(1, alloc_size));
    if (!alloc_data)
        return nullptr;  // Signal out-of-memory by returning null pointer.
    
    hash512* const light_cache = reinterpret_cast<hash512*>(alloc_data + context_alloc_size);
    const hash256 epoch_seed = calculate_epoch_seed(epoch_number);
    build_light_cache(light_cache, light_cache_num_items, epoch_seed);

    uint32_t* const l1_cache =
        reinterpret_cast<uint32_t*>(alloc_data + context_alloc_size + light_cache_size);

    hash1024* full_dataset = isfull ? reinterpret_cast<hash1024*>(l1_cache): nullptr;

    eth_context* const context = new (alloc_data) eth_context{
        epoch_number,
        light_cache_num_items,
        light_cache,
        l1_cache,
        full_dataset_num_items,
        full_dataset,
    };
    if(isfull) {
        multi_threads_init_full_dataset(context);
        // for (uint32_t i = 0; i < l1_cache_size / sizeof(full_dataset_2048[0]); ++i)
        // {
        //     full_dataset_2048[i] = calculate_dataset_item_2048(*context, i);
        // }
    }
    
       
    return context;
}

void destroy_eth_context(eth_context * context)
{
   delete context;
}

const std::shared_ptr<eth_context> get_eth_context(int epoch_number, bool isfull)
{
  if (!shared_eth_context || shared_eth_context->epoch_number != epoch_number){
    shared_eth_context.reset();
    std::lock_guard<std::mutex> lock{shared_eth_context_mutex};
    if (!shared_eth_context || shared_eth_context->epoch_number != epoch_number)
    {
        // Release the shared pointer of the obsoleted context.
        shared_eth_context.reset();

        // Build new context.
        shared_eth_context = std::shared_ptr<eth_context>(create_eth_context(epoch_number, isfull),destroy_eth_context);
    }
  }
  return shared_eth_context;
}

union hash256 keccak256(const uint8_t* data, size_t size)
{
    union hash256 hash;
    keccak(hash.word64s, 256, data, size);
    return hash;
}


union hash512 keccak512(const uint8_t* data, size_t size)
{
    union hash512 hash;
    keccak(hash.word64s, 512, data, size);
    return hash;
}


hash2048 calculate_dataset_item_2048(const eth_context& context, uint32_t index){   
    uint32_t seed[4];
    seed[0] = int64_t(index) * 4;
    seed[1] = int64_t(index) * 4 + 1;
    seed[2] = int64_t(index) * 4 + 2;
    seed[3] = int64_t(index) * 4 + 3;
    hash512 mix[4];
    for(int i =0 ; i < 4; ++i){
        mix[i] = context.light_cache[seed[i] % context.light_cache_num_items];
        mix[i].word32s[0] ^= to_le32(seed[i]);
        mix[i] = keccak512(mix[i].bytes,64);
    }
    
    size_t num_words = sizeof(mix[0]) / sizeof(uint32_t);
    for (uint32_t j = 0; j < full_dataset_item_parents; ++j)
    {
       for(int i =0 ; i < 4; ++i){
        const uint32_t t = fnv1(seed[i] ^ j, mix[i].word32s[j % num_words]);
        const int64_t parent_index = t % context.light_cache_num_items;
        mix[i] = fnv1(mix[i], to_le32(context.light_cache[parent_index]));
       }
    }
    return hash2048{{keccak512(mix[0].bytes, 64), 
                     keccak512(mix[1].bytes, 64), 
                     keccak512(mix[2].bytes, 64), 
                     keccak512(mix[3].bytes, 64)}};
}

hash1024 calculate_dataset_item_1024(const union hash512* const light_cache, const size_t light_cache_num_items, uint32_t index)
{
    uint32_t seed[2];
    seed[0] = int64_t(index) * 2;
    seed[1] = int64_t(index) * 2 + 1;
    hash512 mix[2];
    for(int i =0 ; i < 2; ++i){
        mix[i] = light_cache[seed[i] % light_cache_num_items];
        mix[i].word32s[0] ^= to_le32(seed[i]);
        mix[i] = keccak512(mix[i].bytes,64);
    }
    
    size_t num_words = sizeof(mix[0]) / sizeof(uint32_t);
    for (uint32_t j = 0; j < full_dataset_item_parents; ++j)
    {
       for(int i =0 ; i < 2; ++i){
        const uint32_t t = fnv1(seed[i] ^ j, mix[i].word32s[j % num_words]);
        const int64_t parent_index = t % light_cache_num_items;
        mix[i] = fnv1(mix[i], to_le32(light_cache[parent_index]));
       }
    }
    return hash1024{{keccak512(mix[0].bytes, 64), 
                     keccak512(mix[1].bytes, 64)}};
}


hash512 hash_seed(const hash256& header_hash, uint64_t nonce) 
{
    nonce = to_le64(nonce);
    uint8_t init_data[sizeof(header_hash) + sizeof(nonce)];
    std::memcpy(&init_data[0], &header_hash, sizeof(header_hash));
    std::memcpy(&init_data[sizeof(header_hash)], &nonce, sizeof(nonce));

    return keccak512((const uint8_t*)init_data, sizeof(init_data));
}

hash256 hash_kernel(const union hash512* const light_cache, 
                    const size_t light_cache_num_items,
                    const union hash1024* const full_dataset, 
                    const size_t full_dataset_num_items,
                    const hash512& seed) {
    
    static const size_t num_words = sizeof(hash1024) / sizeof(uint32_t);
    const uint32_t index_limit = full_dataset_num_items;
    const uint32_t seed_init = to_le32(seed.word32s[0]);

    hash1024 mix;
    mix.hash512s[0] =  to_le32(seed);
    mix.hash512s[1] =  to_le32(seed);

    for (uint32_t i = 0; i < num_dataset_accesses; ++i)
    {
        const uint32_t p = fnv1(i ^ seed_init, mix.word32s[i % num_words]) % index_limit;

        hash1024& item = ((hash1024*)full_dataset)[p];

        if (item.word64s[0] == 0)
        {
           item = calculate_dataset_item_1024(light_cache,light_cache_num_items, p);
        }

        const hash1024 newdata = to_le32(item);

        for (size_t j = 0; j < num_words; ++j)
            mix.word32s[j] = fnv1(mix.word32s[j], newdata.word32s[j]);
    }

    hash256 mix_hash;
    for (size_t i = 0; i < num_words; i += 4)
    {
        const uint32_t h1 = fnv1(mix.word32s[i], mix.word32s[i + 1]);
        const uint32_t h2 = fnv1(h1, mix.word32s[i + 2]);
        const uint32_t h3 = fnv1(h2, mix.word32s[i + 3]);
        mix_hash.word32s[i / 4] = h3;
    }
   
    return to_le32(mix_hash);
}


hash256 hash_final(const hash512& seed, const hash256& mix_hash)
{
    uint8_t final_data[sizeof(seed) + sizeof(mix_hash)];
    std::memcpy(&final_data[0], seed.bytes, sizeof(seed));
    std::memcpy(&final_data[sizeof(seed)], mix_hash.bytes, sizeof(mix_hash));
    return keccak256((const uint8_t*)final_data, sizeof(final_data));
}


bool is_less_or_equal(const hash256& a, const hash256& b) 
{
    for (size_t i = 0; i < (sizeof(a) / sizeof(a.word64s[0])); ++i)
    {
        if (__bswap_64(a.word64s[i]) > __bswap_64(b.word64s[i]))
            return false;
        if (__bswap_64(a.word64s[i]) < __bswap_64(b.word64s[i]))
            return true;
    }
    return true;
}

eth_result search(const union hash512* const light_cache, 
        const size_t light_cache_num_items,
        const union hash1024* const full_dataset, 
        const size_t full_dataset_num_items,
        const hash256& header,
        const hash256& boundary,
        uint64_t start_nonce,
        size_t iterations) {
    const uint64_t end_nonce = start_nonce + iterations;
    eth_result result;

    if(full_dataset == nullptr) {
        return result;
    }

    for (uint64_t nonce = start_nonce; nonce < end_nonce; ++nonce)
    {

        const hash512 seed = hash_seed(header, nonce);
        const hash256 mix_hash = hash_kernel(light_cache,light_cache_num_items, full_dataset, full_dataset_num_items , seed);

        const hash256 final_hash = hash_final(seed, mix_hash);

        if (is_less_or_equal(final_hash, boundary)){
        // if(true) {
            result.solution_found = true;
            result.nonce = nonce;
            result.final_hash = final_hash;
            result.mix_hash = mix_hash;
            return result;
        }
    }

    return result;
}

}}